/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *  
 *    http://www.apache.org/licenses/LICENSE-2.0
 *  
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License. 
 *  
 */

package org.apache.mina.protocol.dns.io.decoder;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.mina.common.IoBuffer;
import org.apache.mina.protocol.dns.messages.DnsMessage;
import org.apache.mina.protocol.dns.messages.DnsMessageModifier;
import org.apache.mina.protocol.dns.messages.MessageType;
import org.apache.mina.protocol.dns.messages.OpCode;
import org.apache.mina.protocol.dns.messages.QuestionRecord;
import org.apache.mina.protocol.dns.messages.RecordClass;
import org.apache.mina.protocol.dns.messages.RecordType;
import org.apache.mina.protocol.dns.messages.ResourceRecord;
import org.apache.mina.protocol.dns.messages.ResourceRecordImpl;
import org.apache.mina.protocol.dns.messages.ResponseCode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A decoder for DNS messages.  The primary usage of the DnsMessageDecoder is by
 * calling the <code>decode(IoBuffer)</code> method which will read the 
 * message from the incoming IoBuffer and build a <code>DnsMessage</code>
 * from it according to the DnsMessage encoding in RFC-1035.
 * 
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 * @version $Rev: 84 $, $Date: 2008-02-22 16:33:48 +0000 (Fri, 22 Feb 2008) $
 */
public class DnsMessageDecoder {

    private final Logger logger = LoggerFactory
            .getLogger(DnsMessageDecoder.class);

    /**
     * A Hashed Adapter mapping record types to their encoders.
     */
    private static final Map<RecordType, RecordDecoder> DEFAULT_DECODERS;

    static {
        Map<RecordType, RecordDecoder> map = new HashMap<RecordType, RecordDecoder>();

        map.put(RecordType.A, new AddressRecordDecoder());
        map.put(RecordType.NS, new NameServerRecordDecoder());
        map.put(RecordType.MX, new MailExchangeRecordDecoder());
        map.put(RecordType.AAAA, new IPv6RecordDecoder());
        map.put(RecordType.CNAME, new DefaultDecoder());
        map.put(RecordType.DNAME, new DefaultDecoder());
        map.put(RecordType.SOA, new DefaultDecoder());

        DEFAULT_DECODERS = Collections.unmodifiableMap(map);
    }

    /**
     * Decode the {@link IoBuffer} into a {@link DnsMessage}.
     *
     * @param in
     * @return The {@link DnsMessage}.
     * @throws IOException
     */
    public DnsMessage decode(IoBuffer in) throws IOException {
        DnsMessageModifier modifier = new DnsMessageModifier();

        modifier.setTransactionId(in.getUnsignedShort());

        byte header = in.get();
        modifier.setMessageType(decodeMessageType(header));
        modifier.setOpCode(decodeOpCode(header));
        modifier.setAuthoritativeAnswer(decodeAuthoritativeAnswer(header));
        modifier.setTruncated(decodeTruncated(header));
        modifier.setRecursionDesired(decodeRecursionDesired(header));

        header = in.get();
        modifier.setRecursionAvailable(decodeRecursionAvailable(header));
        modifier.setResponseCode(decodeResponseCode(header));

        short questionCount = in.getShort();
        short answerCount = in.getShort();
        short authorityCount = in.getShort();
        short additionalCount = in.getShort();

        logger.debug("decoding {} question records", questionCount);
        modifier.setQuestionRecords(getQuestions(in, questionCount));

        logger.debug("decoding {} answer records", answerCount);
        modifier.setAnswerRecords(getRecords(in, answerCount));

        logger.debug("decoding {} authority records", authorityCount);
        modifier.setAuthorityRecords(getRecords(in, authorityCount));

        logger.debug("decoding {} additional records", additionalCount);
        modifier.setAdditionalRecords(getRecords(in, additionalCount));

        return modifier.getDnsMessage();
    }

    private List<ResourceRecord> getRecords(IoBuffer IoBuffer, short recordCount)
            throws IOException {
        List<ResourceRecord> records = new ArrayList<ResourceRecord>(
                recordCount);

        for (int ii = 0; ii < recordCount; ii++) {
            String domainName = getDomainName(IoBuffer);
            RecordType recordType = RecordType.convert(IoBuffer.getShort());
            RecordClass recordClass = RecordClass.convert(IoBuffer.getShort());

            int timeToLive = IoBuffer.getInt();
            short dataLength = IoBuffer.getShort();

            Map<String, Object> attributes = decode(IoBuffer, recordType,
                    dataLength);
            records.add(new ResourceRecordImpl(domainName, recordType,
                    recordClass, timeToLive, attributes));
        }

        return records;
    }

    private Map<String, Object> decode(IoBuffer IoBuffer, RecordType type,
            short length) throws IOException {
        RecordDecoder recordDecoder = DEFAULT_DECODERS.get(type);

        if (recordDecoder == null) {
            throw new IllegalArgumentException("Decoder unavailable for "
                    + type);
        }

        return recordDecoder.decode(IoBuffer, length);
    }

    private List<QuestionRecord> getQuestions(IoBuffer IoBuffer,
            short questionCount) {
        List<QuestionRecord> questions = new ArrayList<QuestionRecord>(
                questionCount);

        for (int ii = 0; ii < questionCount; ii++) {
            String domainName = getDomainName(IoBuffer);

            RecordType recordType = RecordType.convert(IoBuffer.getShort());
            RecordClass recordClass = RecordClass.convert((byte) IoBuffer
                    .getShort());

            questions.add(new QuestionRecord(domainName, recordType,
                    recordClass));
        }

        return questions;
    }

    static String getDomainName(IoBuffer IoBuffer) {
        StringBuffer domainName = new StringBuffer();
        recurseDomainName(IoBuffer, domainName);

        return domainName.toString();
    }

    static void recurseDomainName(IoBuffer IoBuffer, StringBuffer domainName) {
        int length = IoBuffer.getUnsigned();

        if (isOffset(length)) {
            int position = IoBuffer.getUnsigned();
            int offset = length & ~(0xc0) << 8;
            int originalPosition = IoBuffer.position();
            IoBuffer.position(position + offset);

            recurseDomainName(IoBuffer, domainName);

            IoBuffer.position(originalPosition);
        } else if (isLabel(length)) {
            int labelLength = length;
            getLabel(IoBuffer, domainName, labelLength);
            recurseDomainName(IoBuffer, domainName);
        }
    }

    static boolean isOffset(int length) {
        return ((length & 0xc0) == 0xc0);
    }

    static boolean isLabel(int length) {
        return (length != 0 && (length & 0xc0) == 0);
    }

    static void getLabel(IoBuffer IoBuffer, StringBuffer domainName,
            int labelLength) {
        for (int jj = 0; jj < labelLength; jj++) {
            char character = (char) IoBuffer.get();
            domainName.append(character);
        }

        if (IoBuffer.get(IoBuffer.position()) != 0) {
            domainName.append(".");
        }
    }

    private MessageType decodeMessageType(byte header) {
        return MessageType.convert((byte) ((header & 0x80) >>> 7));
    }

    private OpCode decodeOpCode(byte header) {
        return OpCode.convert((byte) ((header & 0x78) >>> 3));
    }

    private boolean decodeAuthoritativeAnswer(byte header) {
        return ((header & 0x04) >>> 2) == 1;
    }

    private boolean decodeTruncated(byte header) {
        return ((header & 0x02) >>> 1) == 1;
    }

    private boolean decodeRecursionDesired(byte header) {
        return ((header & 0x01)) == 1;
    }

    private boolean decodeRecursionAvailable(byte header) {
        return ((header & 0x80) >>> 7) == 1;
    }

    private ResponseCode decodeResponseCode(byte header) {
        return ResponseCode.convert((byte) (header & 0x0F));
    }
}
